{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Solutions\n",
    "Simulation results will be different unless you use the seed. Check that your strategy for completing the exercises is similar to the sample solutions here, in that case.\n",
    "\n",
    "## Exercise 1\n",
    "Simulate December 2018 using the seed of 27."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "[INFO] [ simulate.py ] Simulating 31.0 days...\n",
      "[INFO] [ simulate.py ] Saving logs\n",
      "[INFO] [ simulate.py ] All done!\n"
     ]
    }
   ],
   "source": [
    "!python ../../ch_08/simulate.py \\\n",
    "    -s 27 \\\n",
    "    -u ../../ch_08/user_data/user_base.txt \\\n",
    "    -i ../../ch_08/user_data/user_ips.json \\\n",
    "    -l dec_2018_log.csv \\\n",
    "    -hl dec_2018_attacks.csv \\\n",
    "    31 \"2018-12-01\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Imports for Remaining Exercises"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 2\n",
    "Find the number of unique usernames, attempts, successes, failures, and success/failure rates per IP address."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>source_ip</th>\n",
       "      <th>username</th>\n",
       "      <th>success</th>\n",
       "      <th>failures</th>\n",
       "      <th>attempts</th>\n",
       "      <th>success_rate</th>\n",
       "      <th>failure_rate</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1.138.149.116</td>\n",
       "      <td>1</td>\n",
       "      <td>21.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>21.0</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>100.43.18.36</td>\n",
       "      <td>1</td>\n",
       "      <td>15.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>101.113.31.197</td>\n",
       "      <td>1</td>\n",
       "      <td>9.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.000000</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>101.154.143.93</td>\n",
       "      <td>1</td>\n",
       "      <td>28.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>29.0</td>\n",
       "      <td>0.965517</td>\n",
       "      <td>0.034483</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>102.133.183.240</td>\n",
       "      <td>1</td>\n",
       "      <td>6.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>0.000000</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         source_ip  username  success  failures  attempts  success_rate  \\\n",
       "0    1.138.149.116         1     21.0       0.0      21.0      1.000000   \n",
       "1     100.43.18.36         1     15.0       0.0      15.0      1.000000   \n",
       "2   101.113.31.197         1      9.0       0.0       9.0      1.000000   \n",
       "3   101.154.143.93         1     28.0       1.0      29.0      0.965517   \n",
       "4  102.133.183.240         1      6.0       0.0       6.0      1.000000   \n",
       "\n",
       "   failure_rate  \n",
       "0      0.000000  \n",
       "1      0.000000  \n",
       "2      0.000000  \n",
       "3      0.034483  \n",
       "4      0.000000  "
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dec_log = pd.read_csv('dec_2018_log.csv', parse_dates=True, index_col='datetime')\n",
    "\n",
    "log_aggs = dec_log.assign(\n",
    "    failures=lambda x: np.invert(x.success)\n",
    ").groupby('source_ip').agg(\n",
    "    {'username': 'nunique', 'success':'sum', 'failures': 'sum'}\n",
    ").assign(\n",
    "    attempts=lambda x: x.success + x.failures,\n",
    "    success_rate=lambda x: x.success / x.attempts,\n",
    "    failure_rate=lambda x: 1 - x.success_rate\n",
    ").dropna().reset_index()\n",
    "\n",
    "log_aggs.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 3\n",
    "Create two subplots with failures versus attempts on the left and failure rate versus distinct usernames on the right. Draw a decision boundary for what you see. Be sure to color by whether or not it is a hacker IP address."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<matplotlib.lines.Line2D at 0x12de95d0>"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "data": {
      "image/png": "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\n",
      "text/plain": [
       "<Figure size 720x216 with 2 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "is_attack_ip = log_aggs.source_ip.isin(\n",
    "    pd.read_csv('dec_2018_attacks.csv').source_ip\n",
    ")\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(10, 3))\n",
    "\n",
    "for ax, (x, y) in zip(axes, (\n",
    "    ('attempts', 'failures'), ('username', 'failure_rate')\n",
    ")):\n",
    "    ax = sns.scatterplot(\n",
    "        x=log_aggs[x], \n",
    "        y=log_aggs[y], \n",
    "        hue=is_attack_ip,\n",
    "        ax=ax\n",
    "    )\n",
    "    ax.set_title(f'{y.title()} vs. {x.title()}')\n",
    "\n",
    "# boundaries\n",
    "axes[0].plot([0, 80], [80, 0], 'r--')\n",
    "axes[1].axhline(0.5, color='red', linestyle='--')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 4\n",
    "Build a rule-based criteria using percent difference from the median that flags an IP address if failures and attempts are 5 times the median OR if distinct usernames is 5 times the median."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>source_ip</th>\n",
       "      <th>datetime</th>\n",
       "      <th>username</th>\n",
       "      <th>success</th>\n",
       "      <th>failures</th>\n",
       "      <th>attempts</th>\n",
       "      <th>success_rate</th>\n",
       "      <th>failure_rate</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1.138.149.116</td>\n",
       "      <td>2018-12-11 05:00:00</td>\n",
       "      <td>1</td>\n",
       "      <td>4.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>1.138.149.116</td>\n",
       "      <td>2018-12-11 06:00:00</td>\n",
       "      <td>1</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>1.138.149.116</td>\n",
       "      <td>2018-12-19 23:00:00</td>\n",
       "      <td>1</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>1.138.149.116</td>\n",
       "      <td>2018-12-24 16:00:00</td>\n",
       "      <td>1</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>1.138.149.116</td>\n",
       "      <td>2018-12-25 04:00:00</td>\n",
       "      <td>1</td>\n",
       "      <td>3.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "       source_ip            datetime  username  success  failures  attempts  \\\n",
       "0  1.138.149.116 2018-12-11 05:00:00         1      4.0       0.0       4.0   \n",
       "1  1.138.149.116 2018-12-11 06:00:00         1      1.0       0.0       1.0   \n",
       "2  1.138.149.116 2018-12-19 23:00:00         1      1.0       0.0       1.0   \n",
       "3  1.138.149.116 2018-12-24 16:00:00         1      3.0       0.0       3.0   \n",
       "4  1.138.149.116 2018-12-25 04:00:00         1      3.0       0.0       3.0   \n",
       "\n",
       "   success_rate  failure_rate  \n",
       "0           1.0           0.0  \n",
       "1           1.0           0.0  \n",
       "2           1.0           0.0  \n",
       "3           1.0           0.0  \n",
       "4           1.0           0.0  "
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hourly_ip_logs = dec_log.assign(\n",
    "    failures=lambda x: np.invert(x.success)\n",
    ").groupby('source_ip').resample('1H').agg(\n",
    "    {'username': 'nunique', 'success':'sum', 'failures': 'sum'}\n",
    ").assign(\n",
    "    attempts=lambda x: x.success + x.failures,\n",
    "    success_rate=lambda x: x.success / x.attempts,\n",
    "    failure_rate=lambda x: 1 - x.success_rate\n",
    ").dropna().reset_index()\n",
    "\n",
    "hourly_ip_logs.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Function from chapter for getting baselines:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_baselines(hourly_ip_logs, func, *args, **kwargs):\n",
    "    \"\"\"\n",
    "    Calculate hourly bootstrapped statistic per column.\n",
    "    \n",
    "    Parameters:\n",
    "        - hourly_ip_logs: Data to sample from.\n",
    "        - func: Statistic to calculate.\n",
    "        - args: Additional positional arguments for `func`\n",
    "        - kwargs: Additional keyword arguments for `func`\n",
    "    \n",
    "    Returns:\n",
    "        A pandas DataFrame of hourly bootstrapped statistics\n",
    "    \"\"\"\n",
    "    if isinstance(func, str):\n",
    "        func = getattr(pd.DataFrame, func)\n",
    "    return hourly_ip_logs.assign(\n",
    "        hour=lambda x: x.datetime.dt.hour\n",
    "    ).groupby('hour').apply(\n",
    "        lambda x: x.sample(10, random_state=0, replace=True).pipe(func, *args, **kwargs)\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Get baseline:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "medians = get_baselines(hourly_ip_logs, 'median')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Flag if both failures and attempts are 5 times higher than the median or if usernames tried is 5 times higher than the median:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "flagged_ips = hourly_ip_logs.assign(\n",
    "    hour=lambda x: x.datetime.dt.hour\n",
    ").join(\n",
    "    medians, on='hour', rsuffix='_median'\n",
    ").assign(\n",
    "    flag_median=lambda x: np.logical_or(\n",
    "        np.logical_and(\n",
    "            x.failures_median * 5 <= x.failures,\n",
    "            x.attempts_median * 5 <= x.attempts\n",
    "        ), x.username_median * 5 <= x.username\n",
    "    )\n",
    ").query('flag_median').source_ip.drop_duplicates()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise 5\n",
    "Calculate metrics to evaluate how well the ensemble method performed. We can use the `evaluate()` function from the chapter:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def evaluate(alerted_ips, attack_ips, log_ips):\n",
    "    \"\"\"\n",
    "    Calculate true positives (TP), false positives (FP), \n",
    "    true negatives (TN), and false negatives (FN) for \n",
    "    IP addresses flagged as suspicious.\n",
    "    \n",
    "    Parameters:\n",
    "        - alerted_ips: Pandas series of flagged IP addresses\n",
    "        - attack_ips: Pandas series of attacker IP addresses\n",
    "        - log_ips: Pandas series of all IP addresses seen\n",
    "    \n",
    "    Returns:\n",
    "        Tuple of form (TP, FP, TN, FN)\n",
    "    \"\"\"\n",
    "    tp = alerted_ips.isin(attack_ips).sum()\n",
    "    tn = log_ips[log_ips.isin(alerted_ips)].isin(attack_ips).sum()\n",
    "    fp = np.invert(\n",
    "        log_ips[log_ips.isin(alerted_ips)].isin(attack_ips)\n",
    "    ).sum()\n",
    "    fn = np.invert(\n",
    "        log_ips[log_ips.isin(attack_ips)].isin(alerted_ips)\n",
    "    ).sum()\n",
    "    return tp, fp, tn, fn"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next we make a partial to store the attacker IP addreses and the unique IP addresses in the logs:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "# make this easier to call\n",
    "from functools import partial\n",
    "scores = partial(\n",
    "    evaluate, \n",
    "    attack_ips=pd.read_csv('dec_2018_attacks.csv').source_ip, \n",
    "    log_ips=dec_log.source_ip.drop_duplicates()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can evaluate the performance with the `classification_stats()` function from the chapter:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def classification_stats(tp, fp, tn, fn):\n",
    "    \"\"\"Calculate accuracy, precision, recall, and F1-score\"\"\"\n",
    "    recall = tp / (tp + fn)\n",
    "    precision = tp / (tp + fp)\n",
    "    f1_score = 2 * precision * recall / (precision + recall)\n",
    "    return {\n",
    "        'accuracy' : (tp + tn) / (tp + fp + tn + fn),\n",
    "        'precision' : precision,\n",
    "        'recall' : recall,\n",
    "        'F1-score' : f1_score\n",
    "    }"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Performance is pretty good:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'accuracy': 0.918918918918919,\n",
       " 'precision': 0.8717948717948718,\n",
       " 'recall': 0.9714285714285714,\n",
       " 'F1-score': 0.9189189189189189}"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "classification_stats(*scores(flagged_ips))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
